package v1

import (
	"context"
	"fmt"
	"log/slog"
	"regexp"
	"strings"
	"time"

	"quipus/internal/database/model"
	"quipus/internal/utils"
	"quipus/pkg/idp"
	"quipus/pkg/idp/auth/weixin"
	"quipus/pkg/idp/oauth2"

	v1pb "quipus/proto/gen/api/v1"
	apb "quipus/proto/gen/assist"
	cpb "quipus/proto/gen/common"

	"github.com/pkg/errors"
	"github.com/spf13/viper"
	"golang.org/x/crypto/bcrypt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/types/known/emptypb"
	"gorm.io/gorm"
)

func (s *APIV1Service) GetAuthStatus(ctx context.Context, _ *v1pb.GetAuthStatusRequest) (*v1pb.User, error) {
	user, err := s.GetCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
	}
	if user == nil {
		// Set the cookie header to expire access token.
		if err := s.clearAccessTokenCookie(ctx); err != nil {
			return nil, status.Errorf(codes.Internal, "failed to set grpc header: %v", err)
		}
		return nil, status.Errorf(codes.Unauthenticated, "user not found")
	}
	return s.convertUserFromStore(user), nil
}

func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest) (*v1pb.User, error) {
	findUser := &apb.FindUser{
		Username: request.Username,
		TenantId: -1,
	}

	tenantUrn := ""

	if request.TenantName == "" {
		_, tenantName := utils.ExtractUser(request.Username)
		if tenantName != "" {
			request.TenantName = tenantName
		}
	}

	if request.TenantName != "" {
		tenant, err := s.Gds.Handler.GetTenant(ctx, &apb.FindTenant{TenantName: request.TenantName, Id: -1})
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to find tenant %s", request.TenantName))
		}
		// tenantId, err := utils.ExtractIDFromUrn(utils.TenantUrnPrefix, request.TenantUrn)
		// if err != nil {
		// 	return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("failed to extract tenant id from urn %s", request.TenantUrn))
		// }
		findUser.TenantId = tenant.ID
		tenantUrn = utils.BuildUrn(utils.TenantUrnPrefix, tenant.ID)
		findUser.Username = utils.BuildMemberName(request.Username, tenant.TenantName)
	}

	user, err := s.Gds.Handler.GetUser(ctx, findUser)
	if err != nil {
		if err.Error() == gorm.ErrRecordNotFound.Error() {
			return s.SignUp(ctx, &v1pb.SignUpRequest{
				Username:  request.Username,
				Password:  request.Password,
				TenantUrn: tenantUrn,
			})
		}
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to find user by username %s", request.Username))
	}
	if user == nil {
		return s.SignUp(ctx, &v1pb.SignUpRequest{
			Username:  request.Username,
			Password:  request.Password,
			TenantUrn: tenantUrn,
		})
		// return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("user not found with username %s", request.Username))
	} else if user.RowStatus == cpb.RowStatus_ARCHIVED.String() {
		return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("user has been archived with username %s", request.Username))
	}

	// Compare the stored hashed password, with the hashed version of the password that was received.
	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(request.Password)); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "unmatched password")
	}

	expireTime := time.Now().Add(AccessTokenDuration)
	if request.NeverExpire {
		// Set the expire time to 100 years.
		expireTime = time.Now().Add(100 * 365 * 24 * time.Hour)
	}
	if err := s.doSignIn(ctx, user, expireTime); err != nil {
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to sign in, err: %s", err))
	}
	return s.convertUserFromStore(user), nil
}

func convertIdentityProviderConfigFromRaw(idpType string, raw string) (*v1pb.IdentityProviderConfig, error) {
	identityProviderType := cpb.IdpType(cpb.IdpType_value[idpType])
	config := &v1pb.IdentityProviderConfig{}
	if identityProviderType == cpb.IdpType_OAUTH2 {
		oauth2Config := &v1pb.OAuth2Config{}
		if err := protojson.Unmarshal([]byte(raw), oauth2Config); err != nil {
			return nil, errors.Wrap(err, "Failed to unmarshal OAuth2Config")
		}
		config.Config = &v1pb.IdentityProviderConfig_Oauth2Config{Oauth2Config: oauth2Config}
	}
	return config, nil
}

func (s *APIV1Service) SignInWithSSO(ctx context.Context, request *v1pb.SignInWithSSORequest) (*v1pb.User, error) {
	idpType := request.IdpType.String()
	tenantId, err := utils.ExtractIDFromUrn(utils.TenantUrnPrefix, request.TenantUrn)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("failed to extract tenant id from urn %s, err: %s", request.TenantUrn, err))
	}
	identityProvider, err := s.Gds.Handler.GetIdentityProviderByType(ctx, idpType, tenantId)
	if err != nil {
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get identity provider, err: %s", err))
	}
	if identityProvider == nil {
		return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("identity provider not found with Idp Type %d", request.IdpType))
	}

	var userInfo *idp.IdentityProviderUserInfo
	idpConfig, e := convertIdentityProviderConfigFromRaw(identityProvider.Type, identityProvider.Config)
	if e != nil {
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to convert identity provider config, err: %s", e))
	}
	if identityProvider.Type == cpb.IdpType_OAUTH2.String() {
		oauth2IdentityProvider, err := oauth2.NewIdentityProvider(idpConfig.GetOauth2Config())
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create oauth2 identity provider, err: %s", err))
		}
		token, err := oauth2IdentityProvider.ExchangeToken(ctx, request.RedirectUri, request.Code)
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to exchange token, err: %s", err))
		}
		userInfo, err = oauth2IdentityProvider.UserInfo(token)
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get user info, err: %s", err))
		}
	}

	if identityProvider.Type == cpb.IdpType_weixin.String() {
		appSecret := &v1pb.WeixinConfig{}
		key := []byte(viper.GetString("encryption-key"))

		config := identityProvider.Config
		if config != "" {
			config, err = utils.Decrypt(config, key)
		}

		err := protojson.Unmarshal([]byte(config), appSecret)
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to unmarshal app secret config, err: %s", err))
		}
		sk := appSecret.SecretKey
		wx, err := weixin.NewApp(appSecret.AppId, sk)
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create weixin app, err: %s", err))
		}
		openid := wx.AuthCode(ctx, request.Code)
		if userInfo == nil {
			userInfo = &idp.IdentityProviderUserInfo{}
		}
		userInfo.Identifier = openid
	}

	identifierFilter := identityProvider.IdentifierFilter
	if identifierFilter != "" {
		identifierFilterRegex, err := regexp.Compile(identifierFilter)
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to compile identifier filter regex, err: %s", err))
		}
		if !identifierFilterRegex.MatchString(userInfo.Identifier) {
			return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("identifier %s is not allowed", userInfo.Identifier))
		}
	}

	user, err := s.Gds.Handler.GetUser(ctx, &apb.FindUser{
		Username: userInfo.Identifier,
		TenantId: tenantId,
	})

	if err != nil && err != gorm.ErrRecordNotFound {
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to find user by username %s", userInfo.Identifier))
	}

	if user == nil {
		userCreate := &model.User{
			Username: userInfo.Identifier,
			// The new signup user should be normal user by default.
			Nickname: userInfo.DisplayName,
			Email:    userInfo.Email,
		}

		role := cpb.Role_HOST
		if tenantId > 0 {
			userCreate.TenantID = tenantId
			role = cpb.Role_TENANT_ADMIN
		}

		existedSuperUsers, _, err := s.Gds.Handler.ListUsers(ctx, &apb.FindUser{
			Role:     role,
			TenantId: tenantId,
		})

		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to list users, err: %s", err))
		}

		if len(existedSuperUsers) == 0 {
			// Change the default role to host if there is no host user.
			userCreate.Role = role.String()
		} else {
			userCreate.Role = cpb.Role_USER.String()
		}

		password, err := utils.RandomString(20)
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to generate random password, err: %s", err))
		}
		passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to generate password hash, err: %s", err))
		}
		userCreate.PasswordHash = string(passwordHash)
		err = s.Gds.Handler.CreateUser(ctx, userCreate)
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create user, err: %s", err))
		}
		user = userCreate
	}

	if user.RowStatus == cpb.RowStatus_ARCHIVED.String() {
		return nil, status.Errorf(codes.PermissionDenied, fmt.Sprintf("user has been archived with username %s", userInfo.Identifier))
	}

	if err := s.doSignIn(ctx, user, time.Now().Add(AccessTokenDuration)); err != nil {
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to sign in, err: %s", err))
	}
	return s.convertUserFromStore(user), nil
}

func (s *APIV1Service) doSignIn(ctx context.Context, user *model.User, expireTime time.Time) error {
	accessToken, err := GenerateAccessToken(user.Email, user.ID, expireTime, []byte(s.Secret))
	if err != nil {
		return status.Errorf(codes.Internal, fmt.Sprintf("failed to generate tokens, err: %s", err))
	}
	if err := s.UpsertAccessTokenToStore(ctx, user, accessToken, "user login"); err != nil {
		return status.Errorf(codes.Internal, fmt.Sprintf("failed to upsert access token to store, err: %s", err))
	}

	cookie, err := s.buildAccessTokenCookie(ctx, accessToken, expireTime)
	if err != nil {
		return status.Errorf(codes.Internal, fmt.Sprintf("failed to build access token cookie, err: %s", err))
	}

	if err := grpc.SetHeader(ctx, metadata.New(map[string]string{
		"Authorization": GenerateAuthrizationHeader(accessToken),
	})); err != nil {
		return status.Errorf(codes.Internal, "failed to set grpc header, error: %v", err)
	}

	if err := grpc.SetHeader(ctx, metadata.New(map[string]string{
		"Set-Cookie": cookie,
	})); err != nil {
		return status.Errorf(codes.Internal, "failed to set grpc header, error: %v", err)
	}
	return nil
}

func (s *APIV1Service) SignUp(ctx context.Context, request *v1pb.SignUpRequest) (*v1pb.User, error) {
	systemProfile, err := s.GetSystemProfile(ctx, &v1pb.GetSystemProfileRequest{})
	if err != nil {
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get workspace profile, err: %s", err))
	}

	if !systemProfile.Public {
		return nil, status.Errorf(codes.PermissionDenied, "sign up is not allowed")
	}

	passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)
	if err != nil {
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to generate password hash, err: %s", err))
	}

	create := &model.User{
		Username:     request.Username,
		Nickname:     request.Username,
		PasswordHash: string(passwordHash),
	}

	if !utils.UIDMatcher.MatchString(strings.ToLower(create.Username)) {
		return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", create.Username)
	}

	role := cpb.Role_HOST
	tenantId := int32(0)
	tenantName := ""
	if request.TenantUrn != "" {
		tid, err := utils.ExtractIDFromUrn(utils.TenantUrnPrefix, request.TenantUrn)
		if err != nil {
			return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("invalid tenant urn: %s", request.TenantUrn))
		}
		if tid > 0 {
			role = cpb.Role_TENANT_ADMIN
		}
		tenant, e := s.Gds.Handler.GetTenant(ctx, &apb.FindTenant{Id: tid})
		if e != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to get tenant, err: %s", e))
		}
		tenantId = tenant.ID
		tenantName = tenant.TenantName
	}

	existedSuperUsers, _, err := s.Gds.Handler.ListUsers(ctx, &apb.FindUser{
		Role:     role,
		TenantId: tenantId,
	})

	if err != nil {
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to list users, err: %s", err))
	}

	if len(existedSuperUsers) != 0 {
		// Change the default role to host if there is no host user.
		role = cpb.Role_USER
	}

	if role == cpb.Role_HOST {
		tenantId = 0
	} else if tenantId == 0 && role == cpb.Role_USER {
		tenantCreate := &v1pb.CreateTenantRequest{
			// TenantName: shortuuid.New(),
			TenantName: request.Username,
			ShowName:   request.Username}
		tenant, err := s.CreateTenant(ctx, tenantCreate)
		if err != nil {
			return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create tenant, err: %s", err))
		}
		create.TenantID = tenant.Id
		role = cpb.Role_TENANT_ADMIN
	} else {
		create.TenantID = tenantId
		create.Username = utils.BuildMemberName(create.Username, tenantName)
	}

	create.Role = role.String()

	err = s.Gds.Handler.CreateUser(ctx, create)
	if err != nil {
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to create user, err: %s", err))
	}
	if err := s.doSignIn(ctx, create, time.Now().Add(AccessTokenDuration)); err != nil {
		return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to sign in, err: %s", err))
	}

	return s.convertUserFromStore(create), nil
}

func (s *APIV1Service) SignOut(ctx context.Context, _ *v1pb.SignOutRequest) (*emptypb.Empty, error) {
	accessToken, ok := ctx.Value(accessTokenContextKey).(string)
	// Try to delete the access token from the store.
	if ok {
		user, _ := s.GetCurrentUser(ctx)
		if user != nil {
			if _, err := s.DeleteUserAccessToken(ctx, &v1pb.DeleteUserAccessTokenRequest{
				Urn:         utils.BuildUrn(utils.UserUrnPrefix, user.ID),
				AccessToken: accessToken,
			}); err != nil {
				slog.Error("failed to delete access token", "error", err)
			}
		}
	}

	if err := s.clearAccessTokenCookie(ctx); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to set grpc header, error: %v", err)
	}
	return &emptypb.Empty{}, nil
}

func (s *APIV1Service) clearAccessTokenCookie(ctx context.Context) error {
	cookie, err := s.buildAccessTokenCookie(ctx, "", time.Time{})
	if err != nil {
		return errors.Wrap(err, "failed to build access token cookie")
	}
	if err := grpc.SetHeader(ctx, metadata.New(map[string]string{
		"Set-Cookie": cookie,
	})); err != nil {
		return errors.Wrap(err, "failed to set grpc header")
	}
	return nil
}

func (*APIV1Service) buildAccessTokenCookie(ctx context.Context, accessToken string, expireTime time.Time) (string, error) {
	attrs := []string{
		fmt.Sprintf("%s=%s", AccessTokenCookieName, accessToken),
		"Path=/",
		"HttpOnly",
	}
	if expireTime.IsZero() {
		attrs = append(attrs, "Expires=Thu, 01 Jan 1970 00:00:00 GMT")
	} else {
		attrs = append(attrs, "Expires="+expireTime.Format(time.RFC1123))
	}

	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return "", errors.New("failed to get metadata from context")
	}
	var origin string
	for _, v := range md.Get("origin") {
		origin = v
	}
	isHTTPS := strings.HasPrefix(origin, "https://")
	if isHTTPS {
		attrs = append(attrs, "SameSite=None")
		attrs = append(attrs, "Secure")
	} else {
		attrs = append(attrs, "SameSite=Strict")
	}
	return strings.Join(attrs, "; "), nil
}

func (s *APIV1Service) GetCurrentUserId(ctx context.Context) (int32, error) {
	userid, ok := ctx.Value(userIdContextKey).(int32)
	if !ok {
		return 0, errors.Errorf("failed to get user id from context")
	}
	return userid, nil
}

func (s *APIV1Service) GetCurrentUser(ctx context.Context) (*model.User, error) {
	username, ok := ctx.Value(usernameContextKey).(string)
	tenantId, ok := ctx.Value(tenantIdContextKey).(int32)

	if !ok {
		return nil, nil
	}
	user, err := s.Gds.Handler.GetUser(ctx, &apb.FindUser{
		Username: username,
		TenantId: tenantId,
	})
	if err != nil {
		return nil, err
	}

	if user == nil {
		return nil, errors.Errorf("user not found")
	}
	return user, nil
}

func UserCtxClone(ctx context.Context) (context.Context, context.CancelFunc) {
	userid, ok := ctx.Value(userIdContextKey).(int32)
	if !ok {
		return nil, nil
	}
	username, ok := ctx.Value(usernameContextKey).(string)
	if !ok {
		return nil, nil
	}
	tenantId, ok := ctx.Value(tenantIdContextKey).(int32)
	if !ok {
		return nil, nil
	}

	nctx, cancel := context.WithCancel(context.Background())
	nctx = context.WithValue(nctx, userIdContextKey, userid)
	nctx = context.WithValue(nctx, usernameContextKey, username)
	nctx = context.WithValue(nctx, tenantIdContextKey, tenantId)
	return nctx, cancel
}
